/* caConfig.c */

/*
 * Licensed to the ConArtist Development Team (CADT).
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 * The CADT licenses this file to you under the Apache License, 
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <string.h>
#include <stdio.h>

#include <libxml/tree.h>
#include <libxml/parser.h>

#include <apr.h>
#include <apr_dso.h>
#include <apr_file_info.h>
#include <apr_thread_rwlock.h>
#include <apr_strings.h>
#include <apr_fnmatch.h>

#include "conartist.h"
#include "caStore.h"
#include "config.h"
#include "caLog.h"
#include "caGenerator.h"
#include "caPipeline.h"
#include "caMaps.h"

#include "caPrivate.h"

#define SKIP_TEXT_COMMENTS(n)                             \
    if (xmlStrcmp(n->name, (xmlChar *)"comment") == 0 ||  \
        xmlStrcmp(n->name, (xmlChar *)"text") == 0)       \
        continue;

#define TEST_NODE_NAME(n, nm)                             \
    if (xmlStrcmp(n->name, (xmlChar *)nm) == 0)

#define NODE_NAME(n)                                      \
    (char*)xmlGetProp(n, (xmlChar *)"id");


static apr_status_t cfgDSOPath(xmlNode *node);
static apr_status_t cfgLog(xmlNode *node);
static apr_status_t cfgDefaultLogLevel(xmlNode *node);
static apr_status_t cfgCreateStore(xmlNode *node, int local);
static apr_status_t cfgGenerator(xmlNode *node, int local);
static apr_status_t cfgConfigFile(xmlNode *node, int local);
static char *lastErrorMessage = NULL;

/* This produces some odd output, so look for improvements. */
static void xmlErrorFunc(void * ctx, const char * msg, ...)
{
    va_list args;

    va_start(args, msg);
    cavLog(CA_LOG_ERROR, msg, args);
    va_end(args);
}

static void recordError(const char *fmt, ...)
{
    va_list args;
    char *buffer = NULL;

    va_start(args, fmt);
    buffer = apr_pvsprintf(globalPool, fmt, args);
    va_end(args);

    if (lastErrorMessage)
        lastErrorMessage = apr_pstrcat(globalPool, lastErrorMessage,
                                       buffer);
    else
        lastErrorMessage = buffer;

    caLog(CA_LOG_ERROR, buffer);
}

static apr_status_t parseConfigFile(const char *fn, 
                                    struct caGenerator *gen,
                                    int local)
{
    apr_status_t rv = APR_SUCCESS;
    xmlDoc *doc = NULL;
    xmlNode *root = NULL;
    xmlNode *node = NULL;
    char *ptr = NULL;

    if (! fn) {
        recordError("Unable to parse NULL config file");
        return APR_EINVAL;
    }
    ptr = strstr(fn, ":");

    /* Set a default error routine that stops printing to stderr */
    xmlSetGenericErrorFunc(NULL, (xmlGenericErrorFunc)xmlErrorFunc);

    caLog(CA_LOG_INFO, "Parsing configuration file '%s'", fn);

    if (ptr) {
        /* The file is within a store. */
        char *fspath;
        char *store = apr_pstrndup(globalPool, fn, ptr - fn);
        ptr++;
        local = 0;

        if (gen)
            rv = caGeneratorGetFilename(&fspath, ptr, gen);
        else {
            struct caStore *st;
            rv = caStoreFind(&st, store);
            if (rv == APR_SUCCESS)
                rv = (st->module->getPath)(&fspath, ptr, globalPool,
                                           st->privateData);
        }
        if (rv == APR_SUCCESS)
            doc = xmlReadFile(fspath, NULL, 0);
    } else 
        doc = xmlReadFile(fn, NULL, 0);

    if (!doc) {
        recordError("Failed to parse file '%s'", fn);
        rv = APR_EINVAL;
        goto clean_exit;
    }

    root = xmlDocGetRootElement(doc);
    if (!root) {
        recordError("Unable to get root node from file '%s'", fn);
        rv = APR_EINVAL;
        goto clean_exit;
    }

    for (node = root->children; node; node = node->next) {
        SKIP_TEXT_COMMENTS(node)
        /* Processing these can be done from either local or remote
         * configuration files, so they are intentionally limited.
         */
        if (xmlStrcmp(node->name, (xmlChar *)"Store") == 0)
            rv = cfgCreateStore(node, local);
        else TEST_NODE_NAME(node, "Generator")
            rv = cfgGenerator(node, local);
        else TEST_NODE_NAME(node, "ConfigFile")
            rv = cfgConfigFile(node, local);

        if (local) {
            TEST_NODE_NAME(node, "DSOPath")
                rv = cfgDSOPath(node);
            else TEST_NODE_NAME(node, "Log")
                rv = cfgLog(node);
            else TEST_NODE_NAME(node, "DefaultLogLevel")
                rv = cfgDefaultLogLevel(node);
            else TEST_NODE_NAME(node, "CachePath") {
                char *path = (char *)xmlNodeGetContent(node);
                char *cp = apr_pstrdup(globalPool, (char *)path);
                apr_table_set(libConfig, "CachePath", cp);
                free(path);
            }
        }

        if (rv != APR_SUCCESS) {
            recordError("There was an error in node '%s'", 
                        (char *)node->name);
            goto clean_exit;
        }
    }

clean_exit:
    /* If we're not local, ie we've been included from a store, then
     * even if we fail, we return OK as it's not a fatal error. We do
     * log the problem, but we don't fail.
     */
    if (! local && rv != APR_SUCCESS) {
        caLog(CA_LOG_ERROR, "Error parsing configuration file '%s'", fn);
        rv = APR_SUCCESS;
    }

    if (doc)
        xmlFreeDoc(doc);
    return rv;
}

static void caConfigSetInt32(apr_table_t *tbl, const char *key, apr_int32_t val)
{
    char numStr[32];

    if (!tbl || !key)
        return;

    snprintf(numStr, 32, "%d", val);
    apr_table_set(tbl, key, numStr);
}


static apr_status_t cfgDSOPath(xmlNode *node)
{
    char *path = (char *)xmlNodeGetContent(node);
    apr_status_t rv = findDSO(path);
    if (rv != APR_SUCCESS)
        recordError("Unable to read DSO's from '%s'", path);

    xmlFree(path);
    return rv;
}

static apr_status_t cfgLog(xmlNode *node)
{
    char *name = NODE_NAME(node);
    char *path = NULL, *syslog = NULL, *level = NULL;
    xmlNode *n;
    struct caLogFacility *lf = NULL;
    apr_status_t rv = APR_EINVAL;

    if (!name) {
        recordError("Log entry is missing an 'id' attribute");
        return rv;
    }

    for (n = node->children; n; n = n->next) {
        SKIP_TEXT_COMMENTS(n)
        TEST_NODE_NAME(n, "FilePath")
            path = (char *)xmlNodeGetContent(n);
        else TEST_NODE_NAME(n, "SyslogFacility")
            syslog = (char *)xmlNodeGetContent(n);
        else TEST_NODE_NAME(n, "LogLevel")
            level = (char *)xmlNodeGetContent(n);
    }

    if (path && syslog) {
        recordError("Log entries CANNOT contain FilePath and "
                    "SyslogFacility nodes");
        goto clean_exit;
    }
    if (path)
        lf = caOpenLogFile(path, name);
    if (syslog)
        lf = caOpenSyslog(syslog, name);

    if (lf) {
        if (level)
            lf->level = caLogLevelString(level);

        rv = addLogFacility(lf);
        caLog(CA_LOG_INFO, "Log facility %s opened", name);
    }

clean_exit:
    if (path)
        xmlFree(path);
    if (syslog)
        xmlFree(syslog);
    if (level)
        xmlFree(level);
    xmlFree(name);

    return rv;
}

static apr_status_t cfgDefaultLogLevel(xmlNode *node)
{
    char *lvl = (char *)xmlNodeGetContent(node);
    apr_int32_t i = -1;

    if (!lvl)
        return APR_EINVAL;

    i = caLogLevelString(lvl);
    xmlFree(lvl);

    if (i > 0)
        caConfigSetInt32(libConfig, "DEFAULT_LOG_LEVEL", i);

    return APR_SUCCESS;
}

static apr_status_t cfgCreateStore(xmlNode *node, int local)
{
    apr_status_t rv = APR_EINVAL;
    char *type, *name;
    struct caStore *ck = NULL;

    name = NODE_NAME(node);
    type = (char *)xmlGetProp(node, (xmlChar *)"type");
    if (!name || !type) {
        if (!name)
            caLog(CA_LOG_ERROR, "Log config entry has no 'id' attribute");
        else
            caLog(CA_LOG_ERROR, "Log config entry has no 'type' attribute");
        return rv;
    }

    if (caStoreFind(&ck, name) == APR_SUCCESS) {
        caLog(CA_LOG_ERROR, "Duplicate store name '%s'!", name);
        goto clean_exit;
    }

    rv = caStoreOpen(type, name, node);

clean_exit:
    xmlFree(name);
    xmlFree(type);

    return rv;
}

static apr_status_t processPipelineSection(struct caPipeline *p,
                                           xmlNode *node)
{
    xmlNode *n;
    apr_status_t rv = APR_EINVAL;

    for (n = node->children; n; n = n->next) {
        SKIP_TEXT_COMMENTS(n);
        TEST_NODE_NAME(n, "Pipe") {
            char *type = (char *)xmlGetProp(n, (xmlChar *)"type");
            if (type) {
                struct pipeData *pipe;
                /* Create a caPipe structure. This simply creates the
                 * structure.
                 */
                rv = caPipeDataCreate(&pipe, type, n);
                if (rv == APR_SUCCESS)
                    rv = caPipelineAddPipeData(p, pipe);
                xmlFree(type);
            } else {
                caLog(CA_LOG_ERROR, "Pipe elements require a type attribute");
                break;
            }
        } else TEST_NODE_NAME(n, "contentType") {
            xmlChar *data = xmlNodeGetContent(n);
            p->contentType = apr_pstrdup(p->pool, (char *)data);
            xmlFree(data);
        }

    }

    return rv;
}

static apr_status_t processPipeline(struct caGenerator *g, xmlNode *n,
                                    struct caPipeline **rvp)
{
    struct caPipeline *p;
    char *name = NODE_NAME(n);
    apr_status_t rv;

    *rvp = NULL;
    rv = caPipelineCreate(&p, name, g->pool);
 
    if (rv == APR_SUCCESS) {
        /* Process all the nodes within this element as they are
         * part of the peipleine.
         */
        rv = processPipelineSection(p, n);
        /* If we have processed all the entries within the pipeline
         * config section then we register the pipeline.
         */
        if (rv == APR_SUCCESS)
            rv = caPipelineRegister(p, name);
    }
    if (rv == APR_SUCCESS && rvp)
        *rvp = p;
    return rv;
}

static void parseMatchNode(struct caURIMap *mp, xmlNode *node, 
                           apr_pool_t *p)
{
    xmlNode *n;
    
    for (n = node->children; n; n = n->next) {
        xmlChar *data = NULL;
        SKIP_TEXT_COMMENTS(n);
        data = xmlNodeGetContent(n);
        if (data) {
            TEST_NODE_NAME(n, "Path") {
                mp->path = apr_pstrdup(p, (char*)data);
                if (mp->path)
                    mp->pLen = strlen(mp->path);
            } else TEST_NODE_NAME(n, "File")
                mp->filename = apr_pstrdup(p, (char *)data);
            else
                recordError("Unknown node '%s' in Match section");

            xmlFree(data);
        }
    }
}

static void parseOutputNode(struct caURIMap *mp, xmlNode *node,
                            struct caGenerator *gen)
{
    xmlNode *n;
    
    for (n = node->children; n; n = n->next) {
        xmlChar *data = NULL;
        SKIP_TEXT_COMMENTS(n);
        data = xmlNodeGetContent(n);
        if (data) {
            TEST_NODE_NAME(n, "Pipeline")
                caGeneratorFindPipeline(&mp->pi, (char *)data, gen);
            else TEST_NODE_NAME(n, "Filename")
                mp->output = apr_pstrdup(gen->pool, (char *)data);
            xmlFree(data);
        }
    }
}

static apr_status_t processMapping(struct caGenerator *gen, xmlNode *node)
{
    struct caURIMap *mp = NULL;
    xmlNode *n;

    mp = apr_pcalloc(gen->pool, sizeof(*mp));
    if (!mp)
        return APR_ENOMEM;
    mp->deps = apr_hash_make(gen->pool);

    for (n = node->children; n; n = n->next) {
        SKIP_TEXT_COMMENTS(n);
        TEST_NODE_NAME(n, "Match")
            parseMatchNode(mp, n, gen->pool);
        else TEST_NODE_NAME(n, "Output")
            parseOutputNode(mp, n, gen);
        else TEST_NODE_NAME(n, "Depends") {
            char *dep = apr_pstrdup(gen->pool, (char *)xmlNodeGetContent(n));
            if (dep)
                apr_hash_set(mp->deps, dep, APR_HASH_KEY_STRING, dep);
        }
    }

    if (!mp->filename) {
        caLog(CA_LOG_ERROR, "Mapping configuration sections require that "
              "you specify the filename (may be an fnmatch string) to be "
              "processed.");
        return APR_EINVAL;
    }
/*
    if (!mp->path) {
        caLog(CA_LOG_WARN, "No Path specified for mapping of filename '%s'"
              ", assuming '/'", mp->filename);
        mp->path = "/";
    }
*/
    if (!mp->pi && !mp->output) {
        caLog(CA_LOG_ERROR, "Mapping sections MUST specify how to generate "
              "content, either via a pipeline or a filename (or both)");
        return APR_EINVAL;
    }

    /* If we've been given an absolute URI, ie not one with an fnmatch
     * pattern then create the URI string here.
     */    
    if (apr_fnmatch_test(mp->filename) == 0) {
        apr_filepath_merge(&mp->URI, mp->path, mp->filename,
                           APR_FILEPATH_NOTRELATIVE, gen->pool);
    }
printf("MP: path = %s\n    filename = %s\n", mp->path, mp->filename);
//    if (mp->pi)
        return caGeneratorAddMap(mp, gen);

    caLog(CA_LOG_ERROR, "Failed to add a map to the generator");
    return APR_EINVAL;
}

static apr_status_t processHost(struct caGenerator *g, xmlNode *node)
{
    char *hostname = NULL;
    xmlNode *n;
    apr_port_t port = 0;
    apr_status_t rv = APR_SUCCESS;

    for (n = node->children; n; n = n->next) {
        SKIP_TEXT_COMMENTS(n)
        TEST_NODE_NAME(n, "Hostname")
            hostname = (char *)xmlNodeGetContent(n);

        TEST_NODE_NAME(n, "Port") {
            xmlChar *pstr = xmlNodeGetContent(n);
            port = atoi((char *)pstr);
            xmlFree(pstr);
            if (!hostname)
                continue;
            rv = caRegisterHostname(g, hostname, port);
            if (rv != APR_SUCCESS) {
                recordError("Failed to register host '%s:%d'",
                            hostname, port);
                break;
            }
        }
    }

    if (hostname)
        xmlFree(hostname);
    return rv;
}

static apr_status_t cfgGenerator(xmlNode *node, int local)
{
    apr_status_t rv;
    char *name = NODE_NAME(node);
    struct caGenerator *g = NULL;
    xmlNode *n;
    
    if (!name) {
        recordError("No id attribute for Generator section");
        return APR_EINVAL;
    }

    rv = caGeneratorCreate(&g, name);
    if (rv != APR_SUCCESS) {
        recordError("Error creating generator '%s'", name);
        return rv;
    }

    for (n = node->children; n; n = n->next) {
        SKIP_TEXT_COMMENTS(n)

        TEST_NODE_NAME(n, "useStore") {
            char *sname = (char *)xmlGetProp(n, (xmlChar *)"name");
            char *alias = (char *)xmlGetProp(n, (xmlChar *)"alias");
            rv = caGeneratorAddStore(g, alias, sname);
            if (sname)
                xmlFree(sname);
            if (alias)
                xmlFree(alias);
        } else TEST_NODE_NAME(n, "Host") {
            rv = processHost(g, n);
        } else TEST_NODE_NAME(n, "NoCache") {
            apr_table_set(g->cfg, "NoCache", "1");
        } else TEST_NODE_NAME(n, "Pipeline") {
            struct caPipeline *p;
            rv = processPipeline(g, n, &p);
            if (rv == APR_SUCCESS)
                rv = caGeneratorAddPipeline(g, p);
        } else TEST_NODE_NAME(n, "Mapping")
            rv = processMapping(g, n);
        else if (local) {
            TEST_NODE_NAME(n, "CachePath") {
                char *path = (char *)xmlNodeGetContent(n);
                char *cp = apr_pstrdup(globalPool, (char *)path);
                apr_table_set(g->cfg, "CachePath", cp);
                free(path);
            }
        }

        if (rv != APR_SUCCESS) 
            break;
    }

    /* If anything has failed, then we simply return. The generator is
     * created in it's own pool so it'll be cleaned up when we shutdown.
     */
    if (rv != APR_SUCCESS)
        return rv;    

    /* All has gone OK, we need to check that we have a valid cache path
     * available. Create it if required at this point.
     */
    rv = caGeneratorCheckCache(g);
    if (rv != APR_SUCCESS) {
        recordError("Unable to create the cache directory");
    }
    return rv;
}

static apr_status_t cfgConfigFile(xmlNode *node, int local)
{
    apr_status_t rv;
    char *fn = NULL;
    if (! node)
        return APR_EINVAL;

    fn = (char *)xmlNodeGetContent(node);
    if (!fn)
        return APR_EINVAL;
    rv = parseConfigFile(fn, NULL, local);
    xmlFree(fn);
    return rv;
}

/* This function is only called directly, so it's always for a local
 * filename.
 */
apr_status_t caConfigFileParse(const char *fn, apr_pool_t *pool)
{
    return parseConfigFile(fn, NULL, 1);
}

/* Return the error string and reset the error string */
const char *caLastError(void)
{
    const char *rv = lastErrorMessage ? lastErrorMessage : "No Error";
    lastErrorMessage = NULL;
    return rv;
}
